/*-----------------------------------------------------------------------------
	The contents of this file are subject to the Mozilla Public License
	Version 1.1 (the "License"); you may not use this file except in
	compliance with the License. You may obtain a copy of the License at
	http://www.mozilla.org/MPL/
	
	Software distributed under the License is distributed on an "AS IS"
	basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
	License for the specific language governing rights and limitations
	under the License.
	
	The Original Code is Fever Framework code.
	
	The Initial Developer of the Original Code is Romain Ecarnot.
	Portions created by Initial Developer are Copyright (C) 2006
	the Initial Developer. All Rights Reserved.
	
	Contributor(s):
	
	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	See the License for the specific language governing permissions and
	limitations under the License.
-----------------------------------------------------------------------------*/

import org.aswing.ASColor;
import org.aswing.CellEditor;
import org.aswing.Component;
import org.aswing.CursorManager;
import org.aswing.dnd.DragListener;
import org.aswing.dnd.DragManager;
import org.aswing.dnd.SourceData;
import org.aswing.dnd.TreeSourceData;
import org.aswing.geom.Point;
import org.aswing.geom.Rectangle;
import org.aswing.graphics.Graphics;
import org.aswing.graphics.Pen;
import org.aswing.Image;
import org.aswing.JMenuItem;
import org.aswing.JPopupMenu;
import org.aswing.JTree;
import org.aswing.tree.TreePath;
import org.aswing.UIManager;
import org.aswing.util.HashMap;
import org.aswing.util.MCUtils;
import org.aswing.util.Stack;

import com.bourre.commands.Delegate;
import com.bourre.core.HashCodeFactory;

import fever.Fever;
import fever.log.FeverDebug;

import fvaswing.components.cursors.FvCopyCursor;
import fvaswing.components.cursors.FvCursor;
import fvaswing.components.cursors.FvRejectCursor;
import fvaswing.components.tree.FvInvalidDragCursor;
import fvaswing.components.tree.FvTreeBinder;
import fvaswing.components.tree.FvTreeCell;
import fvaswing.components.tree.FvTreeCellFactory;
import fvaswing.components.tree.FvTreeDragImage;
import fvaswing.components.tree.FvTreeItem;
import fvaswing.components.tree.FvTreeModel;
import fvaswing.components.tree.FvTreeNode;
import fvaswing.components.tree.FvTreeResources;
import fvaswing.components.tree.items.FvStringTreeItem;
import fvaswing.FvAsWing;
import fvaswing.utils.FvAsWingDebug;

/**
 * Tree componant.
 * 
 * <p>Extends <strong>AsWing JTree</strong> componant.
 * 
 * <p>Features :
 * <ul>
 *   <li>Support Drag'n Drop process using <italic>#setDragAndDropType()</italic> method
 *   	<ul>
 *   		<li>Allow or not internal Dnd using <italic>#setAutoDragAndDropEnabled()</italic> method</li>
 *   		<li>Can remove dragged node(s) when drop them outside a valid drop target component</li>
 *   		<li>Full mouse cusor process indicator</li>
 *   	</ul>
 *   </li>
 *   <li>Use <italic>fvaswing.componants.tree.FvTreeModel</italic> as base data model.</li> *   <li>Use <italic>fvaswing.componants.tree.FvtreeNode</italic> as base tree node.</li> *   <li>Use <italic>fvaswing.componants.tree.FvTreeItem</italic> as base tree node item content.</li> *   <li>Use <italic>fvaswing.componants.tree.FvTreeCellFactory</italic> as base cell factory.</li>
 *   <li>Support childnodes sorting.( whole tree or specific tree ) using : 
 *   	<ul>
 *   		<li><italic>#sort()</italic> method</li>
 *   		<li><italic>#sortNode()</italic> method</li>
 *   	</ul>
 *   </li>
 *   <li>New <italic>#getSelectionNode()</italic> and <italic>getSelectionNodes()</italic> methods</li>
 *   <li>New removing methods with : 
 *   	<ul>
 *   		<li><italic>#removePath()</italic> method</li> *   		<li><italic>#removePaths()</italic> method</li> *   		<li><italic>#removeNode()</italic> method</li> *   		<li><italic>#removeNodes()</italic> method</li>
 *   	</ul>
 *   </li>
 *    <li>Support expand / collapse tree operations using : 
 *   	<ul>
 *   		<li><italic>#collapseAll()</italic> method</li>
 *   		<li><italic>#expandAll()</italic> method</li>
 *   	</ul>
 *   </li>
 * </ul>
 * 
 * <p>Use <italic>fvaswing.components.tree.FvTreeCellFactory</italic> cell factory ( or extend ) 
 * to add tree node context menu support.
 * 
 * @author Romain Ecarnot
 */
class fvaswing.components.FvTree extends JTree implements DragListener
{
	//-------------------------------------------------------------------------
	// Private properties
	//-------------------------------------------------------------------------
	
	private var _dragAndDropType : Number;
	private var _currentDragAndDropType : Number;
	private var _enabledAutoDragAndDrop : Boolean;
	
	private var _dndListener : Object;	private var _editListener : Object;
	private var _targetPath : TreePath;
	private var _targetRow : Number;
	private var _mcDnd : MovieClip;
	private var _expandInterval : Number;
	private var _removeOnOutside : Boolean;
	private var _preventBeforeRemove : Boolean;
	private var _cloneMap : HashMap;
	
	private var _allowOrderDragging : Boolean;
	private var _dragProcessRunning : Boolean;
	private var _isOutside : Boolean;	private var _expandTimer : Number;
	
	private var _enabledRuntimeCopySelection : Boolean;	private var _copyNodeItemMode : Number;
	private var _copyMenu : JPopupMenu;
	private var _linkMenuItem : JMenuItem;	private var _cloneMenuItem : JMenuItem;
	private var _pendingClone : Object;
	
	private var _resources : FvTreeResources;
	
	
	//-------------------------------------------------------------------------
	// Public Properties
	//-------------------------------------------------------------------------
	
	/** Drag and drop disabled. */
	public static var DND_NONE : Number = DragManager.TYPE_NONE;
	
	/** Drag and drop enabled, and the action of items is move. */
	public static var DND_MOVE : Number = DragManager.TYPE_MOVE;
	
	/** Drag and drop enabled, and the action of items is copy. */
	public static var DND_COPY : Number = DragManager.TYPE_COPY;
	
	/** 
	 * Drag and drop enabled, and the action depend of <strong>Control Key</strong> 
	 * state :
	 * <ul>
	 *   <li>released : move node</li>
	 *   <li>pressed : copy node</li>
	 * </ul>
	 */
	public static var DND_MIX : Number = 3;
	
	/** Time to wait before target node expand himself. ( default is 2000 ms ). */
	public static var DEFAULT_EXPAND_TIMER : Number = 2000;
	
	/** A simple copy ( reference ) is done when user copy a node. */
	public static var ITEM_LINK_MODE : Number = 1;
	
	/** An item content clone is build when user copy a node. ( default ) */
	public static var ITEM_CLONE_MODE : Number = 2;
	 
	
	//-------------------------------------------------------------------------
	// Public API
	//-------------------------------------------------------------------------	
	
	/**
	 * Constructor.
	 */
	public function FvTree( m : FvTreeModel )
	{
		super( ( !m ) ? getEmptyTreeModel() : m );
		
		setCellFactory( new FvTreeCellFactory( new FvTreeCell() ) );
		
		_enabledAutoDragAndDrop = true;
		_allowOrderDragging = false;
		_dragProcessRunning = false;
		_expandTimer = DEFAULT_EXPAND_TIMER;
		_copyNodeItemMode = ITEM_CLONE_MODE;
		
		_resources = FvTreeResources.getInstance();
		
		_enabledRuntimeCopySelection = false;
		_prepareCopyContextMenu();
	}
	
	/**
     * Sets the <code>FvTreeModel</code> that will provide the data.
     *
     * @param newModel the <code>FvTreeModel</code> that is to provide the data
     */
	public function setModel( m : FvTreeModel ) : Void
	{
		super.setModel( m );
	}
	
	/**
     * Returns the <code>FvTreeModel</code> that is providing the data.
     */
	public function getModel() : FvTreeModel
	{
		return FvTreeModel( treeModel );
	}
	
	/**
	 * Sets auto drag and drop type.
	 * 
	 * <p>Available mode are : 
	 * <ul>
	 *   <li><italic>#DND_NONE</italic></li>
	 *   <li><italic>#DND_MOVE</italic></li>
	 *   <li><italic>#DND_COPY</italic></li>
	 * </ul>
	 */
	public function setDragAndDropType( type : Number ) : Void
	{
		_dragAndDropType = type;
		_currentDragAndDropType = ( type != DND_MIX ) ? type : DND_MOVE;
		
		if( _dndListener == null )
		{
			_dndListener = new Object();
			_dndListener[ ON_DRAG_RECOGNIZED ] = Delegate.create( this, _onDragRecognized );
			_dndListener[ ON_DRAG_ENTER ] = Delegate.create( this, _onDragEnter );
			_dndListener[ ON_DRAG_OVERRING ] = Delegate.create( this, _onDragOverring );
			_dndListener[ ON_DRAG_EXIT ] = Delegate.create( this, _onDragExit );
			_dndListener[ ON_DRAG_DROP ] = Delegate.create( this, _onDragDrop );
		}
		
		removeEventListener( _dndListener );
		
		if( _isAutoDragAndDropAllown() )
		{
			setDropTrigger(true);
			setDragEnabled(true);
			addEventListener( _dndListener );
		}
		else
		{
			setDropTrigger(false);
			setDragEnabled(false);
		}
		
		if( _dragAndDropType == DND_MIX ) Key.addListener( this );
		else Key.removeListener( this );
	}
		
	/**
	 * Returns the auto drag and drop type.
	 * 
	 * <p>Return value can be : 
	 * <ul>
	 *   <li><italic>#DND_NONE</italic></li>
	 *   <li><italic>#DND_MOVE</italic></li>
	 *   <li><italic>#DND_COPY</italic></li>
	 * </ul>
	 */
	public function getDragAndDropType() : Number
	{
		if( _dragProcessRunning ) return _currentDragAndDropType;
				return _dragAndDropType;
	}
	
	/**
	 * Allows auto Drag'n Drop.
	 * 
	 * <p>Means allow Drag'n Drop inside the current tree.
	 */
	public function setAutoDragAndDropEnabled( b : Boolean ) : Void
	{
		_enabledAutoDragAndDrop = b;
	}
	
	/**
	 * Returns <code>true</code> if Drag'n Drop is allowed inside 
	 * current component.
	 */
	public function getAutoDragAndDropEnabled() : Boolean
	{
		return _enabledAutoDragAndDrop;
	}
	
	/**
	 * Sets copy item mode.
	 * 
	 * <p>Available mode are : 
	 * <ul>
	 *   <li><italic>#ITEM_LINK_MODE</italic></li>
	 *   <li><italic>#ITEM_CLONE_MODE</italic></li>
	 * </ul>
	 */
	public function setCopyItemMode( n : Number ) : Void
	{
		switch( true )
		{
			case n == ITEM_CLONE_MODE :				case n == ITEM_LINK_MODE : _copyNodeItemMode = n; break;
			default : _copyNodeItemMode = ITEM_CLONE_MODE;
		}
	}
	
	/**
	 * Returns copy item mode.
	 * 
	 * <p>Returns value can be : 
	 * <ul>
	 *   <li><italic>#ITEM_LINK_MODE</italic></li>
	 *   <li><italic>#ITEM_CLONE_MODE</italic></li>
	 * </ul>
	 */
	public function getCopyItemMode(  ) : Number
	{
		return _copyNodeItemMode;	
	}
	
	/**
	 * Defines if user can choose between copy item mode.<br />
	 * Default is false.
	 * 
	 * <p>If <code>true</code> a popupmenu is opened when user press Shift key.
	 */
	public function setRuntimeCopySelection( b : Boolean ) : Void
	{
		_enabledRuntimeCopySelection = b;	
	}
	
	/**
	 * Returns if user can choose between copy item mode.
	 */
	public function getRuntimeCopySelection() : Boolean
	{
		return _enabledRuntimeCopySelection;	
	}
	
	/**
	 * Sets if dropped node(s) can be removed from tree when usser drop them 
	 * outside the tree component.
	 * 
	 * <p>Use {link #setPreventBeforeRemoving()} method to determine if a 
	 * confirmation dialog appear ( or not ) before removing dropped node(s).
	 */
	public function setRemoveOutsideEnabled( b : Boolean ) : Void
	{
		_removeOnOutside = b;	
	}
	
	/**
	 * Returns <code>true</code> if node(s) can be removed when user dropped them 
	 * outside the tree component.
	 * 
	 * <p>Use <italic>#setRemoveOutsideEnabled()</italic> to define this property.
	 */
	public function getRemoveOutsideEnabled() : Boolean
	{
		return _removeOnOutside;	
	}
	
	/**
	 * Indicates if a confirmation dialog appear before removing node 
	 * in <italic>Outside dropping</italic>.
	 */
	public function setPreventRemovingEnabled( b : Boolean ) : Void
	{
		_preventBeforeRemove = b;
	}
	
	/**
	 * Returns <code>true</code> if confirmation dialog is needed.
	 */
	public function getPreventRemovingEnabled() : Boolean
	{
		return _preventBeforeRemove;
	}
	
	/**
	 * Sets if user can arrange nodes order ( in childnode structure ) using 
	 * Dran'n Drop process. ( default is <code>false</code> )
	 */
	public function setOrderArrangementEnabled( b : Boolean ) : Void
	{
		_allowOrderDragging = b;
	}
	
	/**
	 * Returns <code>true</code> if user can arrange nodes order ( in childnode structure ) using 
	 * Dran'n Drop process.
	 */
	public function getOrderArrangementEnabled() : Boolean
	{
		return _allowOrderDragging;	
	}
	
	/**
	 * Sets necessary time before expand a target node during Dnd process.<br />
	 * Must be > 0. 
	 * 
	 * <p>Default is 2000 ms.
	 */
	public function setAutoExpandTimer( n : Number ) : Void
	{
		if( n != undefined && n > 0 ) _expandTimer = n;	
	}
	
	/**
	 * Returns necessary time before expand a target node during Dnd process. 
	 */
	public function getAutoExpandTimer() : Number
	{
		return _expandTimer;
	}
	
	/**
	 * Returns is the source data is acceptale to drop in this tree 
	 * as build-in support
	 */
	public function isAcceptableTreeSourceData( dragInitiator : Component, sd : SourceData ) : Boolean
	{
		return ( sd instanceof TreeSourceData ) && isDragAcceptableInitiator( dragInitiator );
	}
	
	/**
     * Returns true if the tree is editable.
     * 
     * <p>If node is selelected check the node.isEditable() property
     *
     * @return true if the tree is editable
     */
    public function isEditable( ): Boolean
    {
		if( getSelectionNode() != undefined )
		{
			return getSelectionNode().isEditable();
		}
		else return editable;
    }
    
	/**
	 * Sorts all tree nodes.
	 * 
	 * <p>Use <italic>FvTreeModel#setSortingProcess()</italic> method to define 
	 * sorting function.
	 */
	public function sort() : Void
	{
		_storeExpandableMap();
		
		getModel().sort();
		
		_applyExpandableMap();
	}
	
	/**
	 * Sorts passed-in <code>node</code> childnodes.
	 * 
	 * <p>Use <italic>FvTreeModel#setSortingProcess()</italic> method to define 
	 * sorting function.
	 */
	public function sortNode( node : FvTreeNode ) : Void
	{
		getModel().sortNode( node );
	}
	
	/**
	 * Sorts passed-in <code>path</code> treepath.
	 * 
	 * <p>Use <italic>FvTreeModel#setSortingProcess()</italic> method to define 
	 * sorting function.
	 */
	public function sortPath( path : TreePath ) : Void
	{
		getModel().sortNode( FvTreeNode( path.getLastPathComponent() ) );	
	}
	
	/**
	 * Sets if childnodes are automatically reorder ( sort ). 
	 */
	public function setAutoSortingEnabled( b : Boolean ) : Void
	{
		if( b != getModel().getAutoSortingEnabled() )
		{
			getModel().setAutoSortingEnabled( b );
			
			if( b )
			{
				if( _editListener == null )
				{
					_editListener = new Object();
					_editListener[ ON_CELL_EDITING_CANCELED ] = Delegate.create( this, _onEditCanceled );				}
				
				addEventListener( _editListener );
			}
			else removeEventListener( _editListener );
		}
	}
	
	/**
	 * Returns <code>true</code> if auto sorting is enabled.
	 */
	public function getAutoSortingEnabled() : Boolean
	{
		return getModel().getAutoSortingEnabled();
	}
	
	/**
	 * Returns tree node resgitred with passed-in {@code nodeID} in 
	 * current tree.
	 * 
	 * <p>If not, return {@code null}
	 */
	public function getNodeById( nodeID : Number ) : FvTreeNode
	{
		return getModel().getNodeById( nodeID );
	}
	
    /**
     * Returns the first selected node.
     */
	public function getSelectionNode() : FvTreeNode
	{
		var path : TreePath = getSelectionModel().getSelectionPath();
		
		return FvTreeNode( path.getLastPathComponent() );
	}
	
	/**
	 * Returns the node of all selected values.
	 */
	public function getSelectionNodes() : Array
	{
		var a : Array = getSelectionModel().getSelectionPaths();
		var l : Number = a.length;
		var b : Array = new Array();
		
		for ( var i : Number = 0; i < l; i++  ) 
		{
			b.push( FvTreeNode( a[i].getLastPathComponent() ) );
		}
		
		return b;
	}
	
	/**
	 * Removes passed-in <code>path</code> from model.
	 */
	public function removePath( path : TreePath ) : Void
	{
		if( path ) _askForRemoving( new TreeSourceData( 'TreeSourceData', [ path ] ) );
	}
	
	/**
	 * Removes passed-in <code>paths</code> list from tree model.
	 */
	public function removePaths( paths : Array ) : Void
	{
		if( paths.length > 0 ) _askForRemoving( new TreeSourceData( 'TreeSourceData', paths ) );
	}
	
	/**
	 * Removes passed-in <code>node</code> from tree model.
	 */
	public function removeNode( node : FvTreeNode ) : Void
	{
		if( node )
		{
			var path : TreePath = new TreePath( node.getPath() );
			
			_askForRemoving( new TreeSourceData( 'TreeSourceData', [ path ] ) );
		}
	}
	
	/**
	 * Removes passed-in <code>nodes</code> list from tree model.
	 */
	public function removeNodes( nodes : Array ) : Void
	{
		if( nodes.length > 0 ) 
		{
			var a: Array = new Array();
			var l : Number = nodes.length;
			
			for ( var i : Number = 0; i < nodes; i++ ) 
			{
				a.push( new TreePath( nodes[ i ].getPath() ) );
			}
			
			_askForRemoving( new TreeSourceData( 'TreeSourceData', a ) );
		}
	}
	
	/**
	 * Collapse all tree paths.
	 */
	public function collapseAll() : Void
	{
		var nodes : Array = getModel().getRoot().preorderEnumeration();
		var nbItem : Number = nodes.length;
		nodes.reverse();
		
		for( var i : Number = 0; i < nbItem; i++ ) 
		{
			var node : FvTreeNode = nodes[i];
			
			try
			{
				var path : TreePath = new TreePath( node.getPath() );
				if( isExpanded( path ) ) setExpandedState( path, false, false );
			}
			catch( e : Error )
			{
				FvAsWingDebug.FATAL( e.message );
			}
		}
	}
	
	
	/**
	 * Collapse all tree paths.
	 */
	public function expandAll() : Void
	{
		var nodes : Array = getModel().getRoot().preorderEnumeration();
		var nbItem : Number = nodes.length;
		
		for( var i : Number = 0; i < nbItem; i++ ) 
		{
			var node : FvTreeNode = nodes[i];
			
			try
			{
				var path : TreePath = new TreePath( node.getPath() );
				if( isExpanded( path ) ) setExpandedState( path, true, false );
			}
			catch( e : Error )
			{
				FvAsWingDebug.FATAL( e.message );
			}
		}
	}
	
	/** 
	 * Editor has ended editing.
	 */
	public function editingStopped( source : CellEditor ) : Void
	{
		if( isValidEditingValue( source.getCellEditorValue() ) )
		{
			super.editingStopped( source );
		}
		else editingCanceled( source );
	}
	
	/**
	 * Returns {@code true} if {@code newValue} is a valid value for current 
	 * edited cell.
	 * 
	 * <p>Override this method to implement your own value checking process.
	 */
	public function isValidEditingValue( newValue ) : Boolean
	{
		return true;
	}
	
	/**
	 * Null. Implemented for <strong>DragListener</strong> interface.
	 */
	public function onDragStart( dragInitiator : Component, sourceData : SourceData, pos : Point ) : Void
	{
	}

	/**
	 * Null. Implemented for <strong>DragListener</strong> interface.
	 */
	public function onDragEnter( dragInitiator : Component, sourceData : SourceData, pos : Point, targetComponent : Component ) : Void
	{
	}

	/**
	 * Null. Implemented for <strong>DragListener</strong> interface.
	 */
	public function onDragOverring( dragInitiator : Component, sourceData : SourceData, pos : Point, targetComponent : Component ) : Void
	{
	}
	
	/**
	 * Null. Implemented for <strong>DragListener</strong> interface.
	 */
	public function onDragExit( dragInitiator : Component, sourceData : SourceData, pos : Point, targetComponent : Component ) : Void
	{
	}
	
	/**
	 * Called when drag operation finished outside the component.
	 * 
	 * <p>Allow detection of component initiator outside drop.
	 * 
	 * <p>If you overrides method, think about call the super.onDragDrop() method to keep 
	 * outside dropping detection.
	 * 
	 * @param dragInitiator the drag initiator component
	 * @param dragSource the data source
	 * @param pos a Point indicating the cursor location in global space
	 * @param targetComponent dropped component, it may be null if droped on a non-drag-trigger space.
	 */
	public function onDragDrop( dragInitiator : Component, sourceData : SourceData, pos : Point, targetComponent : Component ) : Void
	{
		_onDragDropOutside( targetComponent, dragInitiator, sourceData, pos );
	}
	
	
	//-------------------------------------------------------------------------
	// Private implementation
	//-------------------------------------------------------------------------
	
	 /**
     * Creates and returns a sample <code>FvTreeModel</code>.
     * 
     * <p>Overrides <strong>JTree</strong> method.
     * 
     * @return the default <code>FvTreeModel</code>
     */
    public static function getDefaultTreeModel() : FvTreeModel
    {
        var root : FvTreeNode = new FvTreeNode( new FvStringTreeItem( 'FvTree' ) );
        root.setUserObject( new FvStringTreeItem( 'FvTree' + HashCodeFactory.getKey( root ) ) );
		var parent : FvTreeNode;
		
		parent = new FvTreeNode( new FvStringTreeItem( 'Letters' ) );
		root.append( parent );
		parent.append( new FvTreeNode( new FvStringTreeItem( 'a' ) ) );
		parent.append( new FvTreeNode( new FvStringTreeItem( 'b' ) ) );
		parent.append( new FvTreeNode( new FvStringTreeItem( 'c' ) ) );
		parent.append( new FvTreeNode( new FvStringTreeItem( 'd' ) ) );
		
		parent = new FvTreeNode( new FvStringTreeItem( 'Numbers' ) );
		root.append( parent );
		parent.append( new FvTreeNode( new FvStringTreeItem( '1' ) ) );
		parent.append( new FvTreeNode( new FvStringTreeItem( '2' ) ) );
		parent.append( new FvTreeNode( new FvStringTreeItem( '3' ) ) );
		parent.append( new FvTreeNode( new FvStringTreeItem( '4' ) ) );
		
        return new FvTreeModel( root );
    }
    
    private static function getEmptyTreeModel() : FvTreeModel
    {
        return new FvTreeModel( new FvTreeNode( new FvStringTreeItem( 'tree' ) ) );
    }
    
	private function _isAutoDragAndDropAllown() : Boolean
	{
		return _currentDragAndDropType == DND_MOVE || _currentDragAndDropType == DND_COPY;
	}
	
	/**
     * Overrides JTree#setExpandedState to avoid selection modification during 
     * expand / collapse process
     */
    private function setExpandedState( path : TreePath, state : Boolean, allowSelectionChange : Boolean ) : Void
    {
		if( allowSelectionChange == undefined ) allowSelectionChange = true;
		 
		if( path != null )
		{
		    var stack : Stack;
		    var parentPath : TreePath = path.getParentPath();
			
		    if ( expandedStack.size() == 0 )
		    {
				stack = new Stack();
		    }
		    else
		    {
				stack = Stack(expandedStack.pop());
		    }
	
		    try 
		    {
				while( parentPath != null )
				{
				    if( isExpanded( parentPath ) )
				    {
						parentPath = null;
				    }
				    else 
				    {
						stack.push(parentPath);
						parentPath = parentPath.getParentPath();
				    }
				}
				
				for( var counter : Number = stack.size() - 1; counter >= 0; counter-- )
				{
				    parentPath = TreePath( stack.pop() );
				    
				    if(!isExpanded( parentPath ) )
				    {
						try
						{
						    fireTreeWillExpand( parentPath );
						}
						catch ( prob : Error)
						{
						    return;
						}
						
						expandedState.put(parentPath, true);
						fireTreeExpanded(parentPath);
				    }
				}
		    }
		    catch( e : Error )
		    {
				if (expandedStack.size() < TEMP_STACK_SIZE)
				{
				    stack.clear();
				    expandedStack.push(stack);
				}
		    }
		    
		    if( !state )
		    {
				var cValue:Object = expandedState.get(path);
		
				if( cValue != null && cValue==true )
				{
				    try
				    {
						fireTreeWillCollapse(path);
				    }
				    catch (eve:Error)
				    {
						return;
				    }
				    
				    expandedState.put(path, false);
				    fireTreeCollapsed(path);
				    
				    if ( removeDescendantSelectedPaths(path, false) && !isPathSelected(path) )
				    {
						if( allowSelectionChange )addSelectionPath(path);
				    }
				}
		    }
		    else
		    {
				// Expand last path.
				var cValue:Object = expandedState.get(path);
				
				if(cValue == null || !(cValue==true))
				{
				    try
				    {
						fireTreeWillExpand(path);
				    }
				    catch (eve:Error)
				    {
						return;
				    }
				    expandedState.put(path, true);
				    fireTreeExpanded(path);
				}
		    }
		}
    }
	
	private function _getGlobalMousePosition() : Point
	{
		var mc : MovieClip = FvAsWing.getInstance().getRootMovieClip();
		return new Point( mc._xmouse, mc._ymouse );
	}
	
	private function _askForRemoving( sourceData : TreeSourceData ) : Void
	{
		if( _preventBeforeRemove )
		{
			Fever.dialog.confirm( _resources.deleteSelectionMessage, new Delegate( this, _onConfirmRemoving, sourceData ) );
		}
		else _removeTreeData( sourceData );
	}
	
	private function _onConfirmRemoving( sourceData : TreeSourceData, b : Boolean ) : Void
	{
		if( b ) _removeTreeData( sourceData );
	}
	
	private function _removeTreeData( sourceData : TreeSourceData ) : Void
	{
		_storeExpandableMap();
		
		var hasChanged : Boolean = false;
		var data : Array = sourceData.getPaths();
		var nbItem : Number = data.length;
		var path : TreePath;
		var node : FvTreeNode;
		
		for( var i : Number = 0; i < nbItem; i++ )
		{
			path = TreePath( data[ i ] );
			node = FvTreeNode( path.getLastPathComponent() );
			
			if( node.isDeletable() )
			{
				hasChanged = true;
				
				FvTreeBinder.unbind( node );
				getModel().removeNodeFromParent( node );
			}
		}
		
		if( hasChanged )
		{	
			getModel().reload();
			_applyExpandableMap();
		}		
	}
	
	private function _onDragRecognized( dragInitiator : Component, touchedChild : Component ) : Void
	{
		if( _isAutoDragAndDropAllown() )
		{
			var data : Array = getSelectionPaths();
			var a : Array = new Array();
			var l : Number = data.length;
			
			for (var i : Number = 0; i < l; i++ ) 
			{
				var path : TreePath = TreePath( data[ i ] );
				var node : FvTreeNode = FvTreeNode( path.getLastPathComponent() );
				if( node.isDraggable() ) a.push( path );
			}
			
			if( a.length > 0 )
			{
				var mp : Point = dragInitiator.getMousePosition();
				
				_startDndProcessing();
				
				DragManager.startDrag( 
					this, 
					new TreeSourceData( 'TreeSourceData', a ), 
					new FvTreeDragImage( this, mp.y ),
					this
				);
				
				// don't manage image state as cursor is.
				DragManager.getCurrentDragImage().switchToAcceptImage();
			}
		}
	}
	
	/**
	 * Checks if current Dnd is valid.
	 */
	private function _isAcceptableDragging( dragInitiator : Component, sourceData : SourceData, targetComponent : Component ) : Boolean
	{
		var b : Boolean;
		
		if( _isDragInitiatorIsHimSelf( dragInitiator ) )
		{
			b = _enabledAutoDragAndDrop;
		}
		else b = isAcceptableTreeSourceData( dragInitiator, sourceData );
		
		if( b ) if( _getInitiatorTree( dragInitiator )._checkRootDragging( TreeSourceData( sourceData ) ) ) b = false;
		
		return b; 
	}
	
	/**
	 * Returns {@code true} if current initiator is current tree
	 */
	private function _isDragInitiatorIsHimSelf( dragInitiator : Component ) : Boolean
	{
		return ( getID() == dragInitiator.getID() ); 
	}
	
	/**
	 * Returns {@code true} if current drag item(s) contain root node.
	 */
	private function _checkRootDragging( sourceData : TreeSourceData ) : Boolean
	{
		if( !isRootVisible() ) return false;
		
		var rootNode : FvTreeNode = getModel().getRoot();		var data : Array = sourceData.getPaths();
		var l : Number = data.length;
		
		for ( var i : Number = 0; i < l; i++ ) 
			if( FvTreeNode ( data[ i ].getLastPathComponent() ) == rootNode ) return true;
		
		return false;
	}
	
	/**
	 * Defines outside state of Dnd
	 */
	private function _setOutsideState( b : Boolean ) : Void
	{
		_isOutside = b;	
	}
	
	/**
	 * Returns outside state of Dnd
	 */
	private function _getOutsideState() : Boolean
	{
		return _isOutside;	
	}
	
	/**
	 * Drag enter process
	 */
	private function _onDragEnter( source : Component, dragInitiator : Component, sourceData : SourceData, mousePos : Point ) : Void
	{
		if( _isAcceptableDragging( dragInitiator, sourceData, source ) )
		{
			_getInitiatorTree( dragInitiator )._setOutsideState( false );
			_displayAcceptableDndIndicator( dragInitiator );
			_createDndSupport();
		}
		else
		{
			_displayOutsideDndIndicator( dragInitiator, TreeSourceData( sourceData ) );
		}
	}
	
	/**
	 * Checks on component rollover
	 * 
	 * <p>Caution : be more simple as you can, process is FPS synchronized
	 */
	private function _onDragOverring(source : Component, dragInitiator : Component, sourceData : SourceData, mousePos : Point ) : Void
	{
		if( _isAcceptableDragging( dragInitiator, sourceData, source ) )
		{
			_createDndSupport();
			
			var offsetPoint : Point = getViewPosition();
			var mp : Point = getMousePosition();
			mp.move( offsetPoint.x, offsetPoint.y );
			
			var targetPath : TreePath = getClosestPathForLocation( mp.x, mp.y );			var targetRect : Rectangle = getPathBounds( targetPath );
			
			_targetRow = null;
			_targetPath = null;
			
			if( targetRect.containsPoint( mp ) )
			{
				var targetNode : FvTreeNode = FvTreeNode( targetPath.getLastPathComponent() );
				if( targetNode.getAllowsChildren() && targetNode.getAllowsData( TreeSourceData( sourceData ) )  )
				{
					_displayAcceptableDndIndicator( dragInitiator );
					_checkFuturePathToExpand( targetPath );
					_drawInsertNode( targetRect );
				}
				else
				{
					_displayRejectableDndIndicator();
					_cleanInsertGraphic();
				}
			}
			else
			{
				if( _allowOrderDragging )
				{
					_targetRow = getClosestRowForLocation( mp.x, mp.y );
					
					if( ( _targetRow == 0 && isRootVisible() ) || ( _targetRow == getRowCount() ) )
					{
						_targetRow = null;
						_cleanInsertGraphic();
						_displayRejectableDndIndicator();
						
						return;
					}
					
					//TODO optimization
					var parentNode : FvTreeNode = _getParentNodeFromRowIndex( TreeSourceData( sourceData ), _targetRow ); 
					
					if( parentNode.getAllowsData( TreeSourceData( sourceData ), true, _isDragInitiatorIsHimSelf( dragInitiator ), _getInitiatorTree( dragInitiator ).getDragAndDropType() ) )
					{
						targetRect.width = getInsets().getInsideSize( getSize() ).width - 10;
						_displayAcceptableDndIndicator( dragInitiator );
						_drawInsertLine( _targetRow, targetRect );
					}
					else
					{
						_targetRow = null;
						_cleanInsertGraphic();
						_displayRejectableDndIndicator();
						
						return;
					}
				}
				else
				{
					_cleanInsertGraphic();
					_displayRejectableDndIndicator();
				}
			}
		}
	}
	
	private function _onDragExit(source : Component, dragInitiator : Component, sourceData : SourceData, mousePos : Point ) : Void
	{
		clearInterval( _expandInterval );
		
		_disposeDndSupport();
		_isOutside = true;
		
		// don't manage image state as cursor is.
		// just display accept state for ever.
		DragManager.getCurrentDragImage().switchToAcceptImage();
		
		_displayOutsideDndIndicator( dragInitiator, TreeSourceData( sourceData ) );
	}
	
	private function _onDragDrop( source : Component, dragInitiator : Component, sourceData : SourceData, mousePos : Point ) : Void
	{
		clearInterval( _expandInterval );
		
		if( _isAcceptableDragging( dragInitiator, sourceData, source ) )
		{
			if( _targetPath != null ) _dropSourceDataInNode( dragInitiator, TreeSourceData( sourceData ), _targetPath );
			else if( _targetRow != null )_dropSourceDataAt( dragInitiator, TreeSourceData( sourceData ), _targetRow );
			else FeverDebug.WARN( _debug( 'Dnd is valid but no valid process is found') );
		}
		else
		{
			FeverDebug.ERROR( _debug( 'Dnd is not valid') );
		}
		
		_freeDndCursor();
		_disposeDndSupport();
		_disposeDndProcessing();		
		_getInitiatorTree( dragInitiator )._disposeDndProcessing();
	}
	
	private function _onDragDropOutside( source : Component, dragInitiator : Component, sourceData : SourceData, mousePos : Point ) : Void
	{
		if( _isOutside )
		{
			if( _enabledAutoDragAndDrop )
			{
				if( !dragInitiator.getBounds().containsPoint( _getGlobalMousePosition() ) && !source.isDragAcceptableInitiator( this ) )
				{
					if( ( dragInitiator.getID() != source.getID() ) && !_checkRootDragging( TreeSourceData( sourceData ) ) )
					{
						if( _removeOnOutside )
						{
							DragManager.setDropMotion( DragManager.DEFAULT_DROP_MOTION );
							
							if( !_preventBeforeRemove || Key.isDown( Key.SHIFT ) ) 
							{
								 _removeTreeData( TreeSourceData( sourceData ) );
							}
							else _askForRemoving( TreeSourceData( sourceData ) );
						}
						else DragManager.setDropMotion( DragManager.DEFAULT_REJECT_DROP_MOTION );
					}
				}	
			}
			
			_freeDndCursor();
			_disposeDndSupport();
			_disposeDndProcessing();	
		}
	}
	
	private function _dropSourceDataInNode( dragInitiator : Component, sourceData : TreeSourceData, path : TreePath ) : Void
	{
		var data : Array = sourceData.getPaths();
		var nbItem : Number = data.length;
		
		if( data == null || nbItem <= 0 ) return; // nothing to drop, just return.
		
		var hasChanged : Boolean = false;
		var targetNode : FvTreeNode = FvTreeNode( path.getLastPathComponent() );
		var dropPath : TreePath;
		var dropNode : FvTreeNode;
		var newPath : TreePath;
		
		if( !targetNode.getAllowsChildren() ) 
		{
			_rejectDropMotion();
			return;
		}
		
		_storeExpandableMap();
		
		for ( var i : Number = 0; i < nbItem; i++ ) 
		{
			dropPath = TreePath( data[ i ] );
			dropNode = FvTreeNode( dropPath.getLastPathComponent() );
			
			if( targetNode != dropNode )
			{
				if( _getInitiatorTree( dragInitiator ).getDragAndDropType() == DND_MOVE )
				{
					
					if( !targetNode.isNodeChild( dropNode ) && !dropNode.isNodeDescendant( targetNode ) )
					{
						hasChanged = true;
						
						_getInitiatorTree( dragInitiator ).getModel().removeNodeFromParent( dropNode );
						getModel().insertNodeInto( dropNode, targetNode );
					}
				}
				else if( _getInitiatorTree( dragInitiator ).getDragAndDropType() == DND_COPY )
				{
					if( !dropNode.isNodeDescendant( targetNode ) )
					{
						hasChanged = true;
						
						_cloneNode( _getInitiatorTree( dragInitiator ), dropNode, targetNode );
					}
				}
				
				newPath = new TreePath( dropNode.getPath() );
			}
		}
		
		if( hasChanged )
		{
			getModel().reload();
			
		 	_applyExpandableMap();	
		 	
		 	expandPath( path );
		 	setSelectionPath( newPath );
		}
	}
	
	private function _getParentNodeFromRowIndex( sourceData : TreeSourceData, index : Number ) : FvTreeNode
	{
		var data : Array = sourceData.getPaths();
		var nbItem : Number = data.length;

		var nearestPath : TreePath = getPathForRow( index );
		var parentPath : TreePath = nearestPath.getParentPath();
		return FvTreeNode( parentPath.getLastPathComponent() );			
	}
	
	private function _dropSourceDataAt( dragInitiator : Component, sourceData : TreeSourceData, index : Number ) : Void
	{
		if( index == null || index == undefined || index < 0 ) return;
		
		var data : Array = sourceData.getPaths();
		var nbItem : Number = data.length;
		
		if( data == null || nbItem <= 0 ) return;
		
		_storeExpandableMap();
		
//		var nearestPath : TreePath = getPathForRow( index );
		var parentNode : FvTreeNode = _getParentNodeFromRowIndex( sourceData, index );
		var parentIndex : Number = getRowForPath( new TreePath( parentNode.getPath() ) );
		
		index -= ( parentIndex + 1 ); //add 1 to not count parent himself
		
		var insertOffset : Number = 0;
		var dropPath : TreePath;
		var dropNode : FvTreeNode;
		var newPath : TreePath;
		
		if( _getInitiatorTree( dragInitiator ).getDragAndDropType() == DND_MOVE )
		{
			var initiatorModel : FvTreeModel = _getInitiatorTree( dragInitiator ).getModel();
			var sameModel : Boolean = ( initiatorModel == getModel() );
			
			for( var i : Number = 0; i < nbItem; i++ )
			{
				dropPath = TreePath( data[ i ] );
				dropNode = FvTreeNode( dropPath.getLastPathComponent() );
				
				var nodeIndex : Number = parentNode.getIndex( dropNode );
				
				_getInitiatorTree( dragInitiator ).getModel().removeNodeFromParent( dropNode );
				
				if( sameModel && ( nodeIndex < index ) && ( parentNode == dropNode.getParent() ) )
				{
					insertOffset++;	
				}
			}
		}
		
		index = index - insertOffset;
			
		var storedModel : FvTreeModel = getModel();
		
		for( var i : Number = 0; i < nbItem; i++ )
		{
			dropPath = TreePath( data[ i ] );
			dropNode = FvTreeNode( dropPath.getLastPathComponent() );
			
			_cloneNode( _getInitiatorTree( dragInitiator ), dropNode, parentNode, index );
			
			index++;	
		}
		
		_applyExpandableMap();
	}
	
	private function _cloneNode( dragInitiator : FvTree, oldNode : FvTreeNode, parentNode : FvTreeNode, index : Number ) : Void
	{
		_pendingClone = new Object();
		_pendingClone.initiator = dragInitiator;
		
		if( dragInitiator.getRuntimeCopySelection() && Key.isDown( Key.SHIFT ) )
		{
			_pendingClone.node = oldNode;
			_pendingClone.parent = parentNode;
			_pendingClone.index = index;
			
			_linkMenuItem.setText( _resources.linkNode );			_cloneMenuItem.setText( _resources.cloneNode );
			
			_copyMenu.show( this, getMousePosition().x, getMousePosition().y);
		}
		else _cloneNodeNow( oldNode, parentNode, index );
	}
	
	private function _cloneNodeNow( oldNode : FvTreeNode, parentNode : FvTreeNode, index ) : Void
	{
		var b : Boolean = oldNode.getAllowsChildren();
		var o : FvTreeItem = oldNode.getUserObject();
		
		var newNode : FvTreeNode = oldNode.duplicate();
		var tree : FvTree = FvTree( _pendingClone.initiator );
		
		if( tree.getCopyItemMode() == ITEM_CLONE_MODE )
		{
			newNode.setUserObject( o.clone() );
		}
		else
		{
			newNode.setUserObject( o );
			
			if( !_isDragInitiatorIsHimSelf( tree ) ) 
			{
				FvTreeBinder.bind( o.getIdCode(), newNode.getID(), getID().toString() );				FvTreeBinder.bind( o.getIdCode(), oldNode.getID(), tree.getID().toString() );
			}
		}
		
		getModel().insertNodeInto( newNode, parentNode, index );
		
		if( !oldNode.isLeaf() )
		{
			var childs : Array = oldNode.children();
			var l : Number = childs.length;
			
			for ( var i : Number = 0; i < l; i++ ) 
			{
				_cloneNodeNow( childs[ i ], newNode );
			}
		}		
	}
	
	private function _rejectDropMotion() : Void
	{
		DragManager.setDropMotion( DragManager.DEFAULT_REJECT_DROP_MOTION );
	}
	
	/**
	 * Stores expandable path state.
	 */
	private function _storeExpandableMap() : Void
	{
		_cloneMap = expandedState.clone();
	}
	
	/**
	 * Retreives expandable stable from stored map and 
	 * apply state to whole tree.
	 */
	private function _applyExpandableMap() : Void
	{
		var keys : Array = _cloneMap.keys();
	 	var values : Array = _cloneMap.values();
	 	var l : Number = values.length;
	 	
	 	for ( var i : Number = 0; i < l; i++ ) setExpandedState( keys[ i ], values[ i ] );
	 	
	 	_cloneMap.clear();	
	}
	
	private function _getInitiatorTree( dragInitiator : Component ) : FvTree
	{
		return FvTree( dragInitiator );	
	}
	
	private function _displayAcceptableDndIndicator( dragInitiator : Component ) : Void
	{
		CursorManager.hideCustomCursor();
		
		if( _getInitiatorTree( dragInitiator ).getDragAndDropType() == DND_COPY )
		{
			var cursor : FvCursor = FvCopyCursor.getInstance();
			cursor.arrowVisible = false;
			_displayDndCursor( cursor );
		}
	}
	
	private function _displayRejectableDndIndicator() : Void
	{
		_displayDndCursor( FvInvalidDragCursor.getInstance() );
	}
	
	private function _displayOutsideDndIndicator( dragInitiator : Component, sourceData : TreeSourceData ) : Void
	{
		var tree : FvTree = _getInitiatorTree( dragInitiator );
		if( 
			tree._getOutsideState() && 
			tree.getAutoDragAndDropEnabled() && 
			tree.getRemoveOutsideEnabled() &&
			!tree._checkRootDragging( sourceData )
			)
		{
			var cursor : FvCursor = FvRejectCursor.getInstance();
			cursor.arrowVisible = false;
			_displayDndCursor( cursor );
		}
		else _displayRejectableDndIndicator();
	}
	
	private function _displayDndCursor( cursor : Image ) : Void
	{
		CursorManager.showCustomCursor( cursor );
	}
	
	private function _freeDndCursor() : Void
	{
		CursorManager.hideCustomCursor();
	}
	
	private function _checkFuturePathToExpand( path : TreePath ) : Void
	{
		if( path != _targetPath )
		{
			clearInterval( _expandInterval );
			
			_targetPath = path;
			
			_expandInterval = setInterval( this, '_expandPathIfNecessary', _expandTimer, _targetPath );
		}
	}
	
	private function _expandPathIfNecessary( path : TreePath )  :Void
	{
		clearInterval( _expandInterval );
		
		if( !isExpanded( path ) ) 
		{
			setExpandedState( path, true );	
		}	
	}
	
	private function _createDndSupport() : Void
	{
		if( !MCUtils.isMovieClipExist( _mcDnd ) )
		{
			_mcDnd = createMovieClip( 'line_mc' );
		}
	}
	
	private function _disposeDndSupport() : Void
	{
		if( _mcDnd != null )
		{
			_mcDnd.removeMovieClip();
			_mcDnd = null;
		}
	}
	
	private function _startDndProcessing() : Void 
	{
		FvAsWingDebug.WARN( _debug( 'start Dnd processing' ) );
		
		if( _dragAndDropType == DND_MIX ) _currentDragAndDropType = DND_MOVE;
		_dragProcessRunning = true;
	}
	
	private function _disposeDndProcessing() : Void
	{
		FvAsWingDebug.WARN( _debug( 'dispose Dnd processing' ) );
		
		clearInterval( _expandInterval );
		
		_isOutside = true;
		_dragProcessRunning = false;
		_targetPath = null;
		_targetRow = null;
	}
	
	private function _drawInsertNode( rect : Rectangle ) : Void
	{
		_cleanInsertGraphic();
		
		var g : Graphics = new Graphics( _mcDnd );
		var c : ASColor = UIManager.getColor( 'Tree.selectionBackground' );
		var pen : Pen = new Pen( c.getRGB(), 2, 70 );
		
		rect.y -= getViewPosition().y; 
		
		g.drawRectangle( pen, rect.x, rect.y, rect.width, rect.height );
	}
	
	private function _drawInsertLine( targetRowIndex : Number, rect : Rectangle ) : Void
	{
		_cleanInsertGraphic();
		
		var g:Graphics = new Graphics( _mcDnd );
		var pen:Pen = new Pen( 0, 2, 70 );
		var ty : Number = targetRowIndex * getRowHeight();
		
		ty -= getViewPosition().y;
		
		g.drawLine( pen, rect.x, ty, rect.getRightTop().x, ty );
	}
	
	private function _cleanInsertGraphic() : Void
	{
		_mcDnd.clear();
	}
	
	private function onKeyDown() : Void
	{
		if( _dragAndDropType == DND_MIX && Key.isDown( Key.CONTROL ) )
		{
			if( _currentDragAndDropType != DND_COPY && _dragProcessRunning )
			{
				if( !_isOutside )
				{
					_currentDragAndDropType = DND_COPY;
					_displayDndCursor( FvCopyCursor.getInstance() );
				} 
			}
		}
	}
	
	private function onKeyUp() : Void
	{
		if( _dragAndDropType == DND_MIX && _currentDragAndDropType == DND_COPY )
		{
			if( _currentDragAndDropType != DND_MOVE )
			{
				if( !_isOutside )
				{
					_currentDragAndDropType = DND_MOVE;
					CursorManager.hideCustomCursor();
				}
			}
		}
	}
	
	private function _onEditCanceled( source : FvTree, path : TreePath ) : Void
	{
		if( getModel().getAutoSortingEnabled() ) 
		{
			sortNode( path.getLastPathComponent().getParent() );
		}
	}
	 
	private function _prepareCopyContextMenu() : Void
	{
		_copyMenu = new JPopupMenu( this );
		_linkMenuItem = _copyMenu.addMenuItem( _resources.linkNode );
		_linkMenuItem.addActionListener( Delegate.create( this, _onCopyModeSelected, ITEM_LINK_MODE ) );
		
		_cloneMenuItem = _copyMenu.addMenuItem( _resources.cloneNode );
		_cloneMenuItem.addActionListener(  Delegate.create( this, _onCopyModeSelected, ITEM_CLONE_MODE ) );
	}
	
	private function _onCopyModeSelected( source : JMenuItem, mode : Number ) : Void
	{
		var tree : FvTree = _pendingClone.initiator;
		var tmp : Number = tree.getCopyItemMode();
		
		tree.setCopyItemMode( mode );
		
		_cloneNodeNow( _pendingClone.node, _pendingClone.parent, _pendingClone.index );
		
		_applyExpandableMap();
		
		tree.setCopyItemMode( tmp );
	}
	
	// debug help
	private function _debug( msg : String ) : String
	{
		return getName() + ' [' + getID() + '] -> ' + msg;	
	}
	
}